﻿/*
Axis Labels Plugin for flot. :P
http://github.com/markrcote/flot-axislabels
Released under the GPLv3 license by Xuan Luo, September 2010.
Improvements by Mark Cote.

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

 */
(function ($) {
  var options = {};

  function canvasSupported() {
    return !!document.createElement('canvas').getContext;
  }

  function canvasTextSupported() {
    if (!canvasSupported()) {
      return false;
    }
    var dummy_canvas = document.createElement('canvas');
    var context = dummy_canvas.getContext('2d');
    return typeof context.fillText == 'function';
  }

  function css3TransitionSupported() {
    var div = document.createElement('div');
    return typeof div.style.MozTransition != 'undefined'    // Gecko
        || typeof div.style.OTransition != 'undefined'      // Opera
        || typeof div.style.webkitTransition != 'undefined' // WebKit
        || typeof div.style.transition != 'undefined';
  }


  function AxisLabel(axisName, position, padding, plot, opts) {
    this.axisName = axisName;
    this.position = position;
    this.padding = padding;
    this.plot = plot;
    this.opts = opts;
    this.width = 0;
    this.height = 0;
  }


  CanvasAxisLabel.prototype = new AxisLabel();
  CanvasAxisLabel.prototype.constructor = CanvasAxisLabel;
  function CanvasAxisLabel(axisName, position, padding, plot, opts) {
    AxisLabel.prototype.constructor.call(this, axisName, position, padding,
                                         plot, opts);
  }

  CanvasAxisLabel.prototype.calculateSize = function () {
    if (!this.opts.axisLabelFontSizePixels)
      this.opts.axisLabelFontSizePixels = 14;
    if (!this.opts.axisLabelFontFamily)
      this.opts.axisLabelFontFamily = 'sans-serif';

    var textWidth = this.opts.axisLabelFontSizePixels + this.padding;
    var textHeight = this.opts.axisLabelFontSizePixels + this.padding;
    if (this.position == 'left' || this.position == 'right') {
      this.width = this.opts.axisLabelFontSizePixels + this.padding;
      this.height = 0;
    } else {
      this.width = 0;
      this.height = this.opts.axisLabelFontSizePixels + this.padding;
    }
  };

  CanvasAxisLabel.prototype.draw = function (box) {
    var ctx = this.plot.getCanvas().getContext('2d');
    ctx.save();
    ctx.font = this.opts.axisLabelFontSizePixels + 'px ' +
        this.opts.axisLabelFontFamily;
    var width = ctx.measureText(this.opts.axisLabel).width;
    var height = this.opts.axisLabelFontSizePixels;
    var x, y, angle = 0;
    if (this.position == 'top') {
      x = box.left + box.width / 2 - width / 2;
      y = box.top + height * 0.72;
    } else if (this.position == 'bottom') {
      x = box.left + box.width / 2 - width / 2;
      y = box.top + box.height - height * 0.72;
    } else if (this.position == 'left') {
      x = box.left + height * 0.72;
      y = box.height / 2 + box.top + width / 2;
      angle = -Math.PI / 2;
    } else if (this.position == 'right') {
      x = box.left + box.width - height * 0.72;
      y = box.height / 2 + box.top - width / 2;
      angle = Math.PI / 2;
    }
    ctx.translate(x, y);
    ctx.rotate(angle);
    ctx.fillText(this.opts.axisLabel, 0, 0);
    ctx.restore();
  };


  HtmlAxisLabel.prototype = new AxisLabel();
  HtmlAxisLabel.prototype.constructor = HtmlAxisLabel;
  function HtmlAxisLabel(axisName, position, padding, plot, opts) {
    AxisLabel.prototype.constructor.call(this, axisName, position,
                                         padding, plot, opts);
  }

  HtmlAxisLabel.prototype.calculateSize = function () {
    var elem = $('<div class="axisLabels" style="position:absolute;">' +
                 this.opts.axisLabel + '</div>');
    this.plot.getPlaceholder().append(elem);
    // store height and width of label itself, for use in draw()
    this.labelWidth = elem.outerWidth(true);
    this.labelHeight = elem.outerHeight(true);
    elem.remove();

    this.width = this.height = 0;
    if (this.position == 'left' || this.position == 'right') {
      this.width = this.labelWidth + this.padding;
    } else {
      this.height = this.labelHeight + this.padding;
    }
  };

  HtmlAxisLabel.prototype.draw = function (box) {
    this.plot.getPlaceholder().find('#' + this.axisName + 'Label').remove();
    var elem = $('<div id="' + this.axisName +
                 'Label" " class="axisLabels" style="position:absolute;">'
                 + this.opts.axisLabel + '</div>');
    this.plot.getPlaceholder().append(elem);
    if (this.position == 'top') {
      elem.css('left', box.left + box.width / 2 - this.labelWidth / 2 + 'px');
      elem.css('top', box.top + 'px');
    } else if (this.position == 'bottom') {
      elem.css('left', box.left + box.width / 2 - this.labelWidth / 2 + 'px');
      elem.css('top', box.top + box.height - this.labelHeight + 'px');
    } else if (this.position == 'left') {
      elem.css('top', box.top + box.height / 2 - this.labelHeight / 2 + 'px');
      elem.css('left', box.left + 'px');
    } else if (this.position == 'right') {
      elem.css('top', box.top + box.height / 2 - this.labelHeight / 2 + 'px');
      elem.css('left', box.left + box.width - this.labelWidth + 'px');
    }
  };


  CssTransformAxisLabel.prototype = new HtmlAxisLabel();
  CssTransformAxisLabel.prototype.constructor = CssTransformAxisLabel;
  function CssTransformAxisLabel(axisName, position, padding, plot, opts) {
    HtmlAxisLabel.prototype.constructor.call(this, axisName, position,
                                             padding, plot, opts);
  }

  CssTransformAxisLabel.prototype.calculateSize = function () {
    HtmlAxisLabel.prototype.calculateSize.call(this);
    this.width = this.height = 0;
    if (this.position == 'left' || this.position == 'right') {
      this.width = this.labelHeight + this.padding;
    } else {
      this.height = this.labelHeight + this.padding;
    }
  };

  CssTransformAxisLabel.prototype.transforms = function (degrees, x, y) {
    var stransforms = {
      '-moz-transform': '',
      '-webkit-transform': '',
      '-o-transform': '',
      '-ms-transform': ''
    };
    if (x != 0 || y != 0) {
      var stdTranslate = ' translate(' + x + 'px, ' + y + 'px)';
      stransforms['-moz-transform'] += stdTranslate;
      stransforms['-webkit-transform'] += stdTranslate;
      stransforms['-o-transform'] += stdTranslate;
      stransforms['-ms-transform'] += stdTranslate;
    }
    if (degrees != 0) {
      var rotation = degrees / 90;
      var stdRotate = ' rotate(' + degrees + 'deg)';
      stransforms['-moz-transform'] += stdRotate;
      stransforms['-webkit-transform'] += stdRotate;
      stransforms['-o-transform'] += stdRotate;
      stransforms['-ms-transform'] += stdRotate;
    }
    var s = 'top: 0; left: 0; ';
    for (var prop in stransforms) {
      if (stransforms[prop]) {
        s += prop + ':' + stransforms[prop] + ';';
      }
    }
    s += ';';
    return s;
  };

  CssTransformAxisLabel.prototype.calculateOffsets = function (box) {
    var offsets = { x: 0, y: 0, degrees: 0 };
    if (this.position == 'bottom') {
      offsets.x = box.left + box.width / 2 - this.labelWidth / 2;
      offsets.y = box.top + box.height - this.labelHeight;
    } else if (this.position == 'top') {
      offsets.x = box.left + box.width / 2 - this.labelWidth / 2;
      offsets.y = box.top;
    } else if (this.position == 'left') {
      offsets.degrees = -90;
      offsets.x = box.left - this.labelWidth / 2 + this.labelHeight / 2;
      offsets.y = box.height / 2 + box.top;
    } else if (this.position == 'right') {
      offsets.degrees = 90;
      offsets.x = box.left + box.width - this.labelWidth / 2
                  - this.labelHeight / 2;
      offsets.y = box.height / 2 + box.top;
    }
    return offsets;
  };

  CssTransformAxisLabel.prototype.draw = function (box) {
    this.plot.getPlaceholder().find("." + this.axisName + "Label").remove();
    var offsets = this.calculateOffsets(box);
    var elem = $('<div class="axisLabels ' + this.axisName +
                 'Label" style="position:absolute; ' +
                 this.transforms(offsets.degrees, offsets.x, offsets.y) +
                 '">' + this.opts.axisLabel + '</div>');
    this.plot.getPlaceholder().append(elem);
  };


  IeTransformAxisLabel.prototype = new CssTransformAxisLabel();
  IeTransformAxisLabel.prototype.constructor = IeTransformAxisLabel;
  function IeTransformAxisLabel(axisName, position, padding, plot, opts) {
    CssTransformAxisLabel.prototype.constructor.call(this, axisName,
                                                     position, padding,
                                                     plot, opts);
    this.requiresResize = false;
  }

  IeTransformAxisLabel.prototype.transforms = function (degrees, x, y) {
    // I didn't feel like learning the crazy Matrix stuff, so this uses
    // a combination of the rotation transform and CSS positioning.
    var s = '';
    if (degrees != 0) {
      var rotation = degrees / 90;
      while (rotation < 0) {
        rotation += 4;
      }
      s += ' filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=' + rotation + '); ';
      // see below
      this.requiresResize = (this.position == 'right');
    }
    if (x != 0) {
      s += 'left: ' + x + 'px; ';
    }
    if (y != 0) {
      s += 'top: ' + y + 'px; ';
    }
    return s;
  };

  IeTransformAxisLabel.prototype.calculateOffsets = function (box) {
    var offsets = CssTransformAxisLabel.prototype.calculateOffsets.call(
                      this, box);
    // adjust some values to take into account differences between
    // CSS and IE rotations.
    if (this.position == 'top') {
      // FIXME: not sure why, but placing this exactly at the top causes 
      // the top axis label to flip to the bottom...
      offsets.y = box.top + 1;
    } else if (this.position == 'left') {
      offsets.x = box.left;
      offsets.y = box.height / 2 + box.top - this.labelWidth / 2;
    } else if (this.position == 'right') {
      offsets.x = box.left + box.width - this.labelHeight;
      offsets.y = box.height / 2 + box.top - this.labelWidth / 2;
    }
    return offsets;
  };

  IeTransformAxisLabel.prototype.draw = function (box) {
    CssTransformAxisLabel.prototype.draw.call(this, box);
    if (this.requiresResize) {
      var elem = this.plot.getPlaceholder().find("." + this.axisName + "Label");
      // Since we used CSS positioning instead of transforms for
      // translating the element, and since the positioning is done
      // before any rotations, we have to reset the width and height
      // in case the browser wrapped the text (specifically for the
      // y2axis).
      elem.css('width', this.labelWidth);
      elem.css('height', this.labelHeight);
    }
  };


  function init(plot) {
    // This is kind of a hack. There are no hooks in Flot between
    // the creation and measuring of the ticks (setTicks, measureTickLabels
    // in setupGrid() ) and the drawing of the ticks and plot box
    // (insertAxisLabels in setupGrid() ).
    //
    // Therefore, we use a trick where we run the draw routine twice:
    // the first time to get the tick measurements, so that we can change
    // them, and then have it draw it again.
    var secondPass = false;

    var axisLabels = {};
    var axisOffsetCounts = { left: 0, right: 0, top: 0, bottom: 0 };

    var defaultPadding = 2;  // padding between axis and tick labels
    plot.hooks.draw.push(function (plot, ctx) {
      if (!secondPass) {
        // MEASURE AND SET OPTIONS
        $.each(plot.getAxes(), function (axisName, axis) {
          var opts = axis.options // Flot 0.7
              || plot.getOptions()[axisName]; // Flot 0.6
          if (!opts || !opts.axisLabel)
            return;

          var renderer = null;

          if (!opts.axisLabelUseHtml &&
              navigator.appName == 'Microsoft Internet Explorer') {
            var ua = navigator.userAgent;
            var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
            if (re.exec(ua) != null) {
              rv = parseFloat(RegExp.$1);
            }
            if (rv >= 9 && !opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) {
              renderer = CssTransformAxisLabel;
            } else if (!opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) {
              renderer = IeTransformAxisLabel;
            } else if (opts.axisLabelUseCanvas) {
              renderer = CanvasAxisLabel;
            } else {
              renderer = HtmlAxisLabel;
            }
          } else {
            if (opts.axisLabelUseHtml || (!css3TransitionSupported() && !canvasTextSupported()) && !opts.axisLabelUseCanvas) {
              renderer = HtmlAxisLabel;
            } else if (opts.axisLabelUseCanvas || !css3TransitionSupported()) {
              renderer = CanvasAxisLabel;
            } else {
              renderer = CssTransformAxisLabel;
            }
          }

          var padding = opts.axisLabelPadding === undefined ?
            defaultPadding : opts.axisLabelPadding;

          axisLabels[axisName] = new renderer(axisName,
                                              axis.position, padding,
                                              plot, opts);

          // flot interprets axis.labelHeight and .labelWidth as
          // the height and width of the tick labels. We increase
          // these values to make room for the axis label and
          // padding.

          axisLabels[axisName].calculateSize();

          // AxisLabel.height and .width are the size of the
          // axis label and padding.
          axis.labelHeight += axisLabels[axisName].height;
          axis.labelWidth += axisLabels[axisName].width;
          opts.labelHeight = axis.labelHeight;
          opts.labelWidth = axis.labelWidth;
        });
        // re-draw with new label widths and heights
        secondPass = true;
        plot.setupGrid();
        plot.draw();
      } else {
        // DRAW
        $.each(plot.getAxes(), function (axisName, axis) {
          var opts = axis.options // Flot 0.7
              || plot.getOptions()[axisName]; // Flot 0.6
          if (!opts || !opts.axisLabel)
            return;

          axisLabels[axisName].draw(axis.box);
        });
      }
    });
  }


  $.plot.plugins.push({
    init: init,
    options: options,
    name: 'axisLabels',
    version: '2.0.1'
  });
})(jQuery);
